﻿Imports System
Imports System.Collections
Imports System.Collections.Generic
Imports System.Collections.Specialized
Imports System.Configuration
Imports System.Data.SqlClient
Imports System.Data.SqlTypes
Imports System.Diagnostics
Imports System.Globalization
Imports System.Threading
Imports System.Transactions
Imports System.Workflow.ComponentModel
Imports System.Workflow.Runtime

Namespace System.Workflow.Runtime.Hosting
    Public Class SqlWorkflowPersistenceService
        Inherits WorkflowPersistenceService
        Implements IPendingWork
        ' Methods
        Public Sub New(ByVal parameters As NameValueCollection)
            Me._serviceInstanceId = Guid.Empty
            Me.loadingInterval = New TimeSpan(0, 2, 0)
            Me.maxLoadingInterval = New TimeSpan(&H16D, 0, 0, 0, 0)
            Me.timerLock = New Object
            Me.infinite = New TimeSpan(-1)
            If (parameters Is Nothing) Then
                Throw New ArgumentNullException("parameters", ExecutionStringManager.MissingParameters)
            End If
            Me._ownershipDelta = TimeSpan.MaxValue
            If (Not parameters Is Nothing) Then
                Dim str As String
                For Each str In parameters.Keys
                    If Not str.Equals("ConnectionString", StringComparison.OrdinalIgnoreCase) Then
                        If str.Equals("OwnershipTimeoutSeconds", StringComparison.OrdinalIgnoreCase) Then
                            Dim actualValue As Integer = Convert.ToInt32(parameters.Item("OwnershipTimeoutSeconds"), CultureInfo.CurrentCulture)
                            If (actualValue < 0) Then
                                Throw New ArgumentOutOfRangeException("OwnershipTimeoutSeconds", actualValue, ExecutionStringManager.InvalidOwnershipTimeoutValue)
                            End If
                            Me._ownershipDelta = New TimeSpan(0, 0, actualValue)
                            Me._serviceInstanceId = Guid.NewGuid
                        ElseIf str.Equals("UnloadOnIdle", StringComparison.OrdinalIgnoreCase) Then
                            Me._unloadOnIdle = Boolean.Parse(parameters.Item(str))
                        ElseIf str.Equals("LoadIntervalSeconds", StringComparison.OrdinalIgnoreCase) Then
                            Dim seconds As Integer = Integer.Parse(parameters.Item(str), CultureInfo.CurrentCulture)
                            If (seconds > 0) Then
                                Me.loadingInterval = New TimeSpan(0, 0, seconds)
                            Else
                                Me.loadingInterval = TimeSpan.Zero
                            End If
                            If (Me.loadingInterval > Me.maxLoadingInterval) Then
                                Throw New ArgumentOutOfRangeException("LoadIntervalSeconds", Me.LoadingInterval, ExecutionStringManager.LoadingIntervalTooLarge)
                            End If
                        Else
                            If Not str.Equals("EnableRetries", StringComparison.OrdinalIgnoreCase) Then
                                Throw New ArgumentException(String.Format(Thread.CurrentThread.CurrentCulture, ExecutionStringManager.UnknownConfigurationParameter, New Object() { str }), "parameters")
                            End If
                            Me._enableRetries = Boolean.Parse(parameters.Item(str))
                            Me._ignoreCommonEnableRetries = True
                        End If
                    End If
                Next
            End If
            Me.configParameters = parameters
        End Sub

        Public Sub New(ByVal connectionString As String)
            Me._serviceInstanceId = Guid.Empty
            Me.loadingInterval = New TimeSpan(0, 2, 0)
            Me.maxLoadingInterval = New TimeSpan(&H16D, 0, 0, 0, 0)
            Me.timerLock = New Object
            Me.infinite = New TimeSpan(-1)
            If String.IsNullOrEmpty(connectionString) Then
                Throw New ArgumentNullException("connectionString", ExecutionStringManager.MissingConnectionString)
            End If
            Me.unvalidatedConnectionString = connectionString
        End Sub

        Public Sub New(ByVal connectionString As String, ByVal unloadOnIdle As Boolean, ByVal instanceOwnershipDuration As TimeSpan, ByVal loadingInterval As TimeSpan)
            Me._serviceInstanceId = Guid.Empty
            Me.loadingInterval = New TimeSpan(0, 2, 0)
            Me.maxLoadingInterval = New TimeSpan(&H16D, 0, 0, 0, 0)
            Me.timerLock = New Object
            Me.infinite = New TimeSpan(-1)
            If String.IsNullOrEmpty(connectionString) Then
                Throw New ArgumentNullException("connectionString", ExecutionStringManager.MissingConnectionString)
            End If
            If (loadingInterval > Me.maxLoadingInterval) Then
                Throw New ArgumentOutOfRangeException("loadingInterval", loadingInterval, ExecutionStringManager.LoadingIntervalTooLarge)
            End If
            If (instanceOwnershipDuration < TimeSpan.Zero) Then
                Throw New ArgumentOutOfRangeException("instanceOwnershipDuration", instanceOwnershipDuration, ExecutionStringManager.InvalidOwnershipTimeoutValue)
            End If
            Me._ownershipDelta = instanceOwnershipDuration
            Me._unloadOnIdle = unloadOnIdle
            Me.loadingInterval = loadingInterval
            Me.unvalidatedConnectionString = connectionString
            Me._serviceInstanceId = Guid.NewGuid
        End Sub

        Public Function GetAllWorkflows() As IEnumerable([Of] SqlPersistenceWorkflowInstanceDescription)
            If (MyBase.State = WorkflowRuntimeServiceState.Started) Then
                [Using] accessor As PersistenceDBAccessor = New PersistenceDBAccessor(Me._dbResourceAllocator, Me._enableRetries)
                    Return accessor.RetrieveAllInstanceDescriptions
                End [Using]
            End If
            Throw New InvalidOperationException(String.Format(CultureInfo.CurrentCulture, ExecutionStringManager.WorkflowRuntimeNotStarted, New Object(0  - 1) {}))
        End Function

        Protected Friend Overrides Function LoadCompletedContextActivity(ByVal id As Guid, ByVal outerActivity As Activity) As Activity
            [Using] accessor As PersistenceDBAccessor = New PersistenceDBAccessor(Me._dbResourceAllocator, Me._enableRetries)
                Return WorkflowPersistenceService.RestoreFromDefaultSerializedForm(accessor.RetrieveCompletedScope(id), outerActivity)
            End [Using]
        End Function

        Private Function LoadExpiredTimerIds() As IList([Of] Guid)
            [Using] accessor As PersistenceDBAccessor = New PersistenceDBAccessor(Me._dbResourceAllocator, Me._enableRetries)
                Return accessor.RetrieveExpiredTimerIds(Me._serviceInstanceId, Me.OwnershipTimeout)
            End [Using]
        End Function

        Public Function LoadExpiredTimerWorkflowIds() As IList([Of] Guid)
            If (MyBase.State <> WorkflowRuntimeServiceState.Started) Then
                Throw New InvalidOperationException(String.Format(CultureInfo.CurrentCulture, ExecutionStringManager.WorkflowRuntimeNotStarted, New Object(0  - 1) {}))
            End If
            Return Me.LoadExpiredTimerIds
        End Function

        Protected Friend Overrides Function LoadWorkflowInstanceState(ByVal id As Guid) As Activity
            [Using] accessor As PersistenceDBAccessor = New PersistenceDBAccessor(Me._dbResourceAllocator, Me._enableRetries)
                WorkflowTrace.Host.TraceEvent(TraceEventType.Information, 0, "SqlWorkflowPersistenceService({0}):Loading instance {1}", New Object() { Me._serviceInstanceId.ToString, id.ToString })
                Return WorkflowPersistenceService.RestoreFromDefaultSerializedForm(accessor.RetrieveInstanceState(id, Me._serviceInstanceId, Me.OwnershipTimeout), Nothing)
            End [Using]
        End Function

        Private Sub LoadWorkflowsWithExpiredTimers(ByVal ignored As Object)
            SyncLock Me.timerLock
                If (MyBase.State = WorkflowRuntimeServiceState.Started) Then
                    Dim list As IList([Of] Guid) = Nothing
                    Try 
                        list = Me.LoadExpiredTimerIds
                    Catch exception As Exception
                        MyBase.RaiseServicesExceptionNotHandledEvent(exception, Guid.Empty)
                    End Try
                    If (Not list Is Nothing) Then
                        Dim guid As Guid
                        For Each guid In list
                            WorkflowTrace.Host.TraceEvent(TraceEventType.Information, 0, "SqlWorkflowPersistenceService({1}): Loading instance with expired timers {0}", New Object() { guid, Me._serviceInstanceId.ToString })
                            Try 
                                MyBase.Runtime.GetWorkflow(guid).Load
                                [Continue] For
                            Catch exception4 As WorkflowOwnershipException
                                [Continue] For
                            Catch exception5 As ObjectDisposedException
                                Throw
                            Catch exception2 As InvalidOperationException
                                If Not exception2.Data.Contains("WorkflowNotFound") Then
                                    MyBase.RaiseServicesExceptionNotHandledEvent(exception2, guid)
                                End If
                                [Continue] For
                            Catch exception3 As Exception
                                MyBase.RaiseServicesExceptionNotHandledEvent(exception3, guid)
                                [Continue] For
                            End Try
                        Next
                    End If
                End If
            End SyncLock
        End Sub

        Protected Overrides Sub OnStarted()
            If (Me.loadingInterval > TimeSpan.Zero) Then
                SyncLock Me.timerLock
                    MyBase.OnStarted
                    Me.loadingTimer = New SmartTimer(New TimerCallback(AddressOf Me.LoadWorkflowsWithExpiredTimers), Nothing, Me.loadingInterval, Me.loadingInterval)
                End SyncLock
            End If
            Me.RecoverRunningWorkflowInstances
        End Sub

        Private Sub RecoverRunningWorkflowInstances()
            If (Guid.Empty = Me._serviceInstanceId) Then
                Dim list As IList([Of] Guid) = Nothing
                [Using] accessor As PersistenceDBAccessor = New PersistenceDBAccessor(Me._dbResourceAllocator, Me._enableRetries)
                    list = accessor.RetrieveNonblockingInstanceStateIds(Me._serviceInstanceId, Me.OwnershipTimeout)
                End [Using]
                Dim guid As Guid
                For Each guid In list
                    Try 
                        MyBase.Runtime.GetWorkflow(guid).Load
                        [Continue] For
                    Catch exception As Exception
                        MyBase.RaiseServicesExceptionNotHandledEvent(exception, guid)
                        [Continue] For
                    End Try
                Next
            Else
                [Using] accessor2 As PersistenceDBAccessor = New PersistenceDBAccessor(Me._dbResourceAllocator, Me._enableRetries)
                    Dim guid2 As Guid
                    Do While accessor2.TryRetrieveANonblockingInstanceStateId(Me._serviceInstanceId, Me.OwnershipTimeout, guid2)
                        Try 
                            MyBase.Runtime.GetWorkflow(guid2).Load
                            [Continue] Do
                        Catch exception2 As Exception
                            MyBase.RaiseServicesExceptionNotHandledEvent(exception2, guid2)
                            [Continue] Do
                        End Try
                    Loop
                End [Using]
            End If
        End Sub

        Protected Friend Overrides Sub SaveCompletedContextActivity(ByVal completedScopeActivity As Activity)
            Dim workItem As New PendingWorkItem { _
                .Type = ItemType.CompletedScope, _
                .SerializedActivity = WorkflowPersistenceService.GetDefaultSerializedForm(completedScopeActivity), _
                .InstanceId = WorkflowEnvironment.WorkflowInstanceId, _
                .StateId = DirectCast(completedScopeActivity.GetValue(Activity.ActivityExecutionContextInfoProperty), ActivityExecutionContextInfo).ContextGuid _
            }
            WorkflowEnvironment.WorkBatch.Add(Me, workItem)
        End Sub

        Protected Friend Overrides Sub SaveWorkflowInstanceState(ByVal rootActivity As Activity, ByVal unlock As Boolean)
            If (rootActivity Is Nothing) Then
                Throw New ArgumentNullException("rootActivity")
            End If
            Dim workflowStatus As WorkflowStatus = WorkflowPersistenceService.GetWorkflowStatus(rootActivity)
            Dim isBlocked As Boolean = WorkflowPersistenceService.GetIsBlocked(rootActivity)
            Dim suspendOrTerminateInfo As String = WorkflowPersistenceService.GetSuspendOrTerminateInfo(rootActivity)
            Dim guid As Guid = DirectCast(rootActivity.GetValue(Activity.ActivityContextGuidProperty), Guid)
            Dim workItem As New PendingWorkItem { _
                .Type = ItemType.Instance, _
                .InstanceId = WorkflowEnvironment.WorkflowInstanceId _
            }
            If ((workflowStatus <> WorkflowStatus.Completed) AndAlso (workflowStatus <> WorkflowStatus.Terminated)) Then
                workItem.SerializedActivity = WorkflowPersistenceService.GetDefaultSerializedForm(rootActivity)
            Else
                workItem.SerializedActivity = New Byte(0  - 1) {}
            End If
            workItem.Status = CInt(workflowStatus)
            workItem.Blocked = If(isBlocked, 1, 0)
            workItem.Info = suspendOrTerminateInfo
            workItem.StateId = guid
            workItem.Unlocked = unlock
            Dim subscription As TimerEventSubscription = DirectCast(rootActivity.GetValue(TimerEventSubscriptionCollection.TimerCollectionProperty), TimerEventSubscriptionCollection).Peek
            workItem.NextTimer = If((subscription Is Nothing), SqlDateTime.MaxValue, subscription.ExpiresAt)
            If (workItem.Info Is Nothing) Then
                workItem.Info = ""
            End If
            WorkflowTrace.Host.TraceEvent(TraceEventType.Information, 0, "SqlWorkflowPersistenceService({4}):Committing instance {0}, Blocked={1}, Unlocked={2}, NextTimer={3}", New Object() { guid.ToString, workItem.Blocked, workItem.Unlocked, workItem.NextTimer.Value.ToLocalTime, Me._serviceInstanceId.ToString })
            WorkflowEnvironment.WorkBatch.Add(Me, workItem)
        End Sub

        Protected Friend Overrides Sub Start()
            WorkflowTrace.Host.TraceEvent(TraceEventType.Information, 0, "SqlWorkflowPersistenceService({1}): Starting, LoadInternalSeconds={0}", New Object() { Me.loadingInterval.TotalSeconds, Me._serviceInstanceId.ToString })
            Me._dbResourceAllocator = New DbResourceAllocator(MyBase.Runtime, Me.configParameters, Me.unvalidatedConnectionString)
            Me._transactionService = MyBase.Runtime.GetService([Of] WorkflowCommitWorkBatchService)
            Me._dbResourceAllocator.DetectSharedConnectionConflict(Me._transactionService)
            If (Not Me._ignoreCommonEnableRetries AndAlso (Not MyBase.Runtime Is Nothing)) Then
                Dim commonParameters As NameValueConfigurationCollection = MyBase.Runtime.CommonParameters
                If (Not commonParameters Is Nothing) Then
                    Dim str As String
                    For Each str In commonParameters.AllKeys
                        If (String.Compare("EnableRetries", str, StringComparison.OrdinalIgnoreCase) = 0) Then
                            Me._enableRetries = Boolean.Parse(commonParameters.Item(str).Value)
                            Exit For
                        End If
                    Next
                End If
            End If
            MyBase.Start
        End Sub

        Protected Friend Overrides Sub [Stop]()
            WorkflowTrace.Host.TraceEvent(TraceEventType.Information, 0, "SqlWorkflowPersistenceService({0}): Stopping", New Object() { Me._serviceInstanceId.ToString })
            SyncLock Me.timerLock
                MyBase.Stop
                If (Not Me.loadingTimer Is Nothing) Then
                    Me.loadingTimer.Dispose
                    Me.loadingTimer = Nothing
                End If
            End SyncLock
        End Sub

        Private Sub System.Workflow.Runtime.IPendingWork.Commit(ByVal transaction As Transaction, ByVal items As ICollection) Implements IPendingWork.Commit
            Dim accessor As PersistenceDBAccessor = Nothing
            Try 
                accessor = New PersistenceDBAccessor(Me._dbResourceAllocator, transaction, Me._transactionService)
                Dim item As PendingWorkItem
                For Each item In items
                    Select Case item.Type
                        Case ItemType.Instance
                            accessor.InsertInstanceState(item, Me._serviceInstanceId, Me.OwnershipTimeout)
                            [Continue]
                        Case ItemType.CompletedScope
                            accessor.InsertCompletedScope(item.InstanceId, item.StateId, item.SerializedActivity)
                            [Continue]
                        Case ItemType.ActivationComplete
                            accessor.ActivationComplete(item.InstanceId, Me._serviceInstanceId)
                            [Continue]
                    End Select
                Next
            Catch exception As SqlException
                WorkflowTrace.Runtime.TraceEvent(TraceEventType.Error, 0, "SqlWorkflowPersistenceService({1})Exception thrown while persisting instance: {0}", New Object() { exception.Message, Me._serviceInstanceId.ToString })
                WorkflowTrace.Runtime.TraceEvent(TraceEventType.Error, 0, "stacktrace : {0}", New Object() { exception.StackTrace })
                If (exception.Number = SqlWorkflowPersistenceService._deadlock) Then
                    Dim exception2 As New PersistenceException(exception.Message, exception)
                    Throw exception2
                End If
                Throw
            Catch exception3 As Exception
                WorkflowTrace.Runtime.TraceEvent(TraceEventType.Error, 0, "SqlWorkflowPersistenceService({1}): Exception thrown while persisting instance: {0}", New Object() { exception3.Message, Me._serviceInstanceId.ToString })
                WorkflowTrace.Runtime.TraceEvent(TraceEventType.Error, 0, "stacktrace : {0}", New Object() { exception3.StackTrace })
                Throw exception3
            Finally
                If (Not accessor Is Nothing) Then
                    accessor.Dispose
                End If
            End Try
        End Sub

        Private Sub System.Workflow.Runtime.IPendingWork.Complete(ByVal succeeded As Boolean, ByVal items As ICollection) Implements IPendingWork.Complete
            If ((Not Me.loadingTimer Is Nothing) AndAlso succeeded) Then
                Dim item As PendingWorkItem
                For Each item In items
                    If item.Type.Equals(ItemType.Instance) Then
                        Me.loadingTimer.Update(CDate(item.NextTimer))
                    End If
                Next
            End If
        End Sub

        Private Function System.Workflow.Runtime.IPendingWork.MustCommit(ByVal items As ICollection) As Boolean Implements IPendingWork.MustCommit
            Return True
        End Function

        Protected Friend Overrides Function UnloadOnIdle(ByVal activity As Activity) As Boolean
            Return Me._unloadOnIdle
        End Function

        Protected Friend Overrides Sub UnlockWorkflowInstanceState(ByVal rootActivity As Activity)
            Dim workItem As New PendingWorkItem { _
                .Type = ItemType.ActivationComplete, _
                .InstanceId = WorkflowEnvironment.WorkflowInstanceId _
            }
            WorkflowEnvironment.WorkBatch.Add(Me, workItem)
            WorkflowTrace.Host.TraceEvent(TraceEventType.Information, 0, "SqlWorkflowPersistenceService({0}):Unlocking instance {1}", New Object() { Me._serviceInstanceId.ToString, workItem.InstanceId.ToString })
        End Sub


        ' Properties
        Public Property EnableRetries As Boolean
            Get
                Return Me._enableRetries
            End Get
            Set(ByVal value As Boolean)
                Me._enableRetries = value
                Me._ignoreCommonEnableRetries = True
            End Set
        End Property

        Public ReadOnly Property LoadingInterval As TimeSpan
            Get
                Return Me.loadingInterval
            End Get
        End Property

        Private ReadOnly Property OwnershipTimeout As DateTime
            Get
                If (Me._ownershipDelta = TimeSpan.MaxValue) Then
                    Return DateTime.MaxValue
                End If
                Return (DateTime.UtcNow + Me._ownershipDelta)
            End Get
        End Property

        Public ReadOnly Property ServiceInstanceId As Guid
            Get
                Return Me._serviceInstanceId
            End Get
        End Property


        ' Fields
        Private _dbResourceAllocator As DbResourceAllocator
        Private Shared _deadlock As Integer = &H4B5
        Private _enableRetries As Boolean
        Private _ignoreCommonEnableRetries As Boolean
        Private _ownershipDelta As TimeSpan
        Private _serviceInstanceId As Guid
        Private _transactionService As WorkflowCommitWorkBatchService
        Private _unloadOnIdle As Boolean
        Private configParameters As NameValueCollection
        Private Const EnableRetriesToken As String = "EnableRetries"
        Private infinite As TimeSpan
        Private Const InstanceOwnershipTimeoutSecondsToken As String = "OwnershipTimeoutSeconds"
        Private loadingInterval As TimeSpan
        Private Const LoadingIntervalToken As String = "LoadIntervalSeconds"
        Private loadingTimer As SmartTimer
        Private maxLoadingInterval As TimeSpan
        Private timerLock As Object
        Private Const UnloadOnIdleToken As String = "UnloadOnIdle"
        Private unvalidatedConnectionString As String
    End Class
End Namespace

